#!/usr/bin/python
######
#       httpmodule.py
#       Copyright Etelos Inc. 2007
#       Darien Hager
#       darien@etelos.com
######

import urllib, httplib, os, time, thread, socket
from httplib import HTTPSConnection
import threading 

class ReagentHTTPError(Exception):

	TYPE_UNKNOWN = 0
	
	TYPE_HTTP = 1 # Network, HTTP error
	
	TYPE_WEB = 2 # Good HTTP response, but PHP or URL error
	
	TYPE_BADINPUT = 3
	TYPE_BADDATA = 4
	
	TYPE_NOTFOUND = 5
	TYPE_CANTWRITE = 6
	TYPE_CANTREAD = 7
	
	TYPE_DB_ERROR = 8
	
	TYPE_NOTIMPLEMENTED = 9
	
	
	
	MAPPING = {'servererror':2,
	'badinput':3,
	'baddata':4,
	'notfound':5,
	'cantwrite':6,
	'cantread':7,
	'db-error':8,
	'notimplemented':9	
	}
		
	
	def __init__(self,type,message):		
		ty = type.lower()
		if(ty in self.MAPPING.keys()):			
			ty = self.MAPPING[ty]
		else:
			if(ty != 1):
				ty = 0
		self.type = ty
		self.message = message
	def __str__(self):
		return "("+repr(self.type)+") "+self.message

class fileResponse:
	def __init__(self,name,modtime,data):
		self.name = name
		self.modtime = modtime
		self.data = data
		
class reagentResponse:
	""" A simple object that stores the results of a request to the server. 
	These objects are not usually created if a network or HTTP error occurred,
	only if the agent managed to successfully contact the server-side script."""
	
	
	def __init__(self,htresp, debuglevel = 0):
		""" Creates a new response object, passing in HTTP headers (Module rfc822 :: Class Message)
		and the HTTP response (plain text) """
		
		self.time = 0	# When the request was fulfilled
		
		self.cmd = ""	# The command string given to the server (echoed back by serv)
		
		self.error = ""	# Error code, if any. Generally capital letters and underscores.
		
		self.errormsg = ""	# Descriptive error message
		
		self.version = 0	# Server script ver	
	
		self.data = '' # The raw data of the HTML response
	
		self.files = []
	
		self.modtime = "" # For when checking mod time for a single file
		
		self.misc = {}	# Any other headers provided.
				

		
		##-----------
		
		
		## self.data[''] = htresp.read()[:-1] 
		self.data = htresp.read()		
		if(debuglevel > 1):
			print "Debug: Original HTTP Response body".center(80,"=")
			print self.data
			print "--".center(80,"=")

		tuplist = htresp.getheaders()
		ver = htresp.getheader('reagent-version')
		if ver != None:
			self.version = ver
		else:
			# Something is VERY wrong! Most likely there was a PHP
			# error and the server is presenting us a blank page.
			# So while there are no HTTP or network errors for this,
			# the lack of this must-always-exist header is a sign
			# that the system is down.
			self.cmd = "[SERVER ERROR]"
			self.version = "???"
			self.error = "SERVERERROR"
			self.errormsg = "This message was generated by httpagent. "
			self.errormsg += "The server is not responding with the expected headers. " 
			self.errormsg += "Possibly there is a PHP error. " 
			

			

		
		self.misc = {}		
		for tup in tuplist:		
			
			(name,val) = tup
			if not name.startswith("reagent-") :
				continue; # Skip non-reagent headers
			# Loop through all of our reagent-* keys. If there are
			# any that require special treatment, pull them out and
			# process them.
			name = name.split("reagent-")[1]; # Split off the prefix
				
			if(name == "error"):
				# Parse out error messages...
				error_temp = val
				parts = error_temp.split(":",1)
				self.error = parts[0]
				self.errormsg = parts[1]
			if(name == "timestamp"):
				# Store timestamp...
				self.time = val
			elif(name == "responding"):
				# What is the server responding to? (Great for some debugs.)
				self.cmd = val				
			elif(name == "version"):
				# We did this earlier as a sanity check. 
				# Skip it. 
				pass
			elif(name == "modtime"):
				self.modtime = val
				
			elif(name == "split"):
				# Here's an example of some split data
				# id_number:0-4:1159221411,name:4-24,1159221411
				
				# In the example, two properties are identified
				# (id_number and name) and the first spans the
				# first four bytes of the data, the second spanning
				# the next 20 bytes. (zero-indexed). The final values
				# are their respective timestamps
				# You split first by commas, then by semicolons
				
				split_temp = val
				split_parts = split_temp.split(",")
				for b in split_parts:
					if(b == ""):
						continue						
					(name,range, timestamp) = b.split(":")
					(start,stop) = range.split("-")
					start = int(start)
					stop = int(stop)
					# Add the rest of the data stuff
					if(stop-start < 1):
						# Blank, no content
						self.files.append(fileResponse(name,int(timestamp),"")			)			
					else:
						# Not blank!
						self.files.append(fileResponse(name,int(timestamp),self.data[start:stop]))					
			else:
				# For all the others, put them in an array for
				# future reference.
				self.misc[name] = val

	def printMeta(self):
		"""Prints everything but the .data[''] property of this object """
		print "Timestamp:\t",self.time
		print "Serv ver:\t",self.version
		print "Command Used:\t",self.cmd
		print "Err Type:\t",self.error
		print "Err Msg:\t",self.errormsg
		print "Modtime(s):\t",self.modtime
		print "Files:\t",self.files
		print "Extra info:\t",self.misc		
			
		
class httpmodule:
	""" Serves to intermediate between the server-side scripts and the local
	logic. """
	
	MAXDELTAS = 10
	MAXTRIES = 5	
	
	def __init__(self,authdata,debuglev = 0):
		
		
		
		#self.__PHPFILE = "/darien/reagent/reagent.esp"
		self.__PHPFILE = "/sys/opendev/reagent.esp"
		self.__timedeltas = []
		

		
		self.__lock = threading.RLock()
		
		#-------------------------
		
		self.__debuglevel = debuglev
		(self.__host,self.__user,self.__pass) = authdata
		
		self.__makeConnection()
		
		msg = self.__checkConnection()
		if(msg != ""):
			raise IOError, msg
	
	
	def __makeConnection(self):	
		self.__conn = HTTPSConnection(self.__host)
		self.__conn.set_debuglevel(self.__debuglevel)
	def __checkConnection(self):
		""" Checks the hostname and credentials of this user.
		Returns "" on success, or an error message on failure. """
		resp = self.__doReq("auth","check")
		if(resp == None):
			return self.error		
		elif (resp.error != "") :			
			return resp.error + ": "+  resp.errormsg				
		else:
			return ""
		
		#TODO make this also where the client grabs the server-stored
		#subscription list?
	def __updateTime(self,local,remote):
		""" Update our stored time data with the given time as retrieved
		from a reagentResponse object. """
	
		diff = int(remote) - local
		self.__timedeltas.append(diff)
		while(len(self.__timedeltas) > self.MAXDELTAS):
			self.__timedeltas.pop(0)
		
	def setDebugLevel(self,lev):
		if(type(lev) != int):
			assert False
			return False
		self.__lock.acquire()
		self.__debuglevel = lev
		self.__conn.set_debuglevel(lev)
		self.__lock.release()
		return True
	def timeDelta(self):
		""" Returns the number of seconds to add to local timestamps to
		achieve a near-equivalent timestamp on the server. """		
		sum = 0
		count = 0
		self.__lock.acquire()
		for delta in self.__timedeltas:
			sum += delta
			count += 1
		self.__lock.release()
		if(count > 0):		
			return int(sum/count)
		else:
			self.__lock.release()
			return 0;
	def __doReq(self,action,ftype,fpath = "",data = {},tries = 0):
		
		""" Commits a request to the server script, and creates a reagentResponse object or exception on failure."""
		
		headers = {"User-Agent":'REAgent/1.0 +http://www.etelos.com/',
		"Content-type": "application/x-www-form-urlencoded",
		"Accept": "text/plain"}
		params = {"reagent-user":self.__user,
		"reagent-pass":self.__pass,
		"reagent-action":action,
		"reagent-type":ftype,
		"reagent-path":fpath.replace(os.sep,"/")
		}
		
		
		for key in data.keys():
			if(key.startswith("reagent-")):
				params[key] = data[key]
			else:
				params["reagent-"+key] = data[key]
		try: # Bug previous to Python 2.5, "try-except" must be nested within "try-finally", no "try-except-finally" allowed.								 			
			try:
				tries += 1
				self.__lock.acquire() # Prevent multiple threads from barging in
				ltime = time.time()
				self.__conn.request("POST",self.__PHPFILE,urllib.urlencode(params),headers)						
				httpresponse = self.__conn.getresponse()
				if(httpresponse.status == 200):											
					obj = reagentResponse(httpresponse,self.__debuglevel)										
					self.__updateTime(ltime,obj.time)					
					# Check if error needs to be raised
					if(obj.error == ""):						
						if(self.__debuglevel > 0):
							obj.printMeta()
						return obj
					else:		
						raise ReagentHTTPError(obj.error,obj.errormsg)				

				else:	
					raise ReagentHTTPError('unknown',str(httpresponse.status)+": "+str(httpresponse.reason))				
			except httplib.CannotSendRequest, e:
				# Our connection has probably timed out. 
				self.__makeConnection() 
				if(tries < self.MAXTRIES ):
					if(tries > 2):
						# If our first retry works, let's not scare anybody.
						print("NETWORK ERROR: 'CannotSendRequest'. Attempt #"+str(tries+1)+"...")
					return self.__doReq(action,ftype,fpath,data,tries)
				else:
					return None
			except httplib.BadStatusLine, e:			
				self.error = "HTTP Error: Bad HTTP status line ("+str(e)+")"
				# We don't (yet???) have any operations which are harmful if repeated twice.
				# this is a relatively common and sporadic error, so try again...
				if(tries < self.MAXTRIES ):
					if(tries > 2):
						# If our first retry works, let's not scare anybody.
						print("NETWORK ERROR: 'BadStatusLine'. Attempt #"+str(tries+1)+"...")				
					return self.__doReq(action,ftype,fpath,data,tries)
				else:
					return None
			except httplib.HTTPException, e:
				assert False, e
				# Superclass of other httplib errors
				self.error = "General Network Error:\t("+str(type(e))+')\t'+str(e)			
				# We don't (yet???) have any operations which are harmful if repeated twice.
				# this is a relatively common and sporadic error, so try again...
				if(tries < self.MAXTRIES ):
					if(tries > 2):
						print("GENERAL NETWORK ERROR: Attempt #"+str(tries+1)+"...")				
					return self.__doReq(action,ftype,fpath,data,tries)
				else:
					return None	
			except socket.error, e:
				# VERY uncommon... Treat it like a retry
				assert False, e 
				self.error = "Low-level socket error:\t("+str(type(e))+')\t'+str(e)			
				# We don't (yet???) have any operations which are harmful if repeated twice.
				# this is a relatively common and sporadic error, so try again...
				if(tries < self.MAXTRIES ):
					print("SOCKET ERROR: Attempt #"+str(tries+1)+"...")				
					return self.__doReq(action,ftype,fpath,data,tries)
				else:
					return None	
		finally:
			self.__lock.release()
			

	
	def specialGuiList(self,bits):
		ret = []		
		resp = self.__doReq("special",'guilist',"/".join(bits))
		if((resp == None) or (resp.error != "")):
			assert False
			return ret
		if(resp.data[-1:] == "\n"): resp.data = resp.data[:-1] # Strip last newline
		items = resp.data.split("\n")
		for i in items:
			parts = i.split("\t")
			ret.append(tuple(parts))
		return ret
		

	def specialSchemaSum(self,typelist = []):
		ret = {}
		try:
			resp = self.__doReq("special","schemasum",",".join(typelist))
			
		except ReagentHTTPError, e:
			if((e.type == e.TYPE_NOTFOUND) or (e.type == e.TYPE_BADINPUT)):
				return errval
			else:
				raise e
				
		tables = resp.data.split("\n")
		for t in tables:
			if(len(t) < 1):
				continue # Skip it, probably a trailing newline too
			parts = t.split("\t")
			ret[parts[0]] = parts[1]
		return ret
				
	def specialPubIssues(self,pubid):
		""" Returns Issue ID and issue names for a given publication """
		resp = self.__doReq("special","pubissues",str(pubid))
		if(resp.data[-1:] == "\n"): resp.data = resp.data[:-1] # Strip last newline
		lines = resp.data.split("\n")
		ret = []
		for line in lines:
			parts = line.split("\t")
			ret.append(parts[0])
		return ret		
	def specialIssueOwner(self,issueid):
		resp = self.__doReq("special","issuepub",str(issueid))		
		try:
			r = int(resp.data)
			return resp.data
		except ValueError:
			return None	
	def checkRemote(self,pathbits):
		errval = None
		try:
			resp = self.__doReq("check",pathbits[0],"/".join(pathbits[1:])) # Paths sent to server must use unix-style directories
		except ReagentHTTPError, e:
			if((e.type == e.TYPE_NOTFOUND) or (e.type == e.TYPE_BADINPUT)):
				return errval
			else:
				raise e
		if len(resp.files) == 1:
			return resp.files[0].modtime
		else:
			return errval				
			
	def getRemote(self,pathbits):
		val = self.__doReq("get",pathbits[0],"/".join(pathbits[1:])) # Paths sent to server must use unix-style directories		
		return val.files
			
		return val.files	
	def listRemote(self,pathbits):				
		if(len(pathbits) > 1):
			resp = self.__doReq("list",pathbits[0],"/".join(pathbits[1:]))
		elif(len(pathbits) == 0):
			assert False, "Invalid, no path data given"
			return None
		else:
			resp = self.__doReq("list",pathbits[0],'')
		# Most of the list commands tend to output a hanging newline--let's
		# take steps to fix that.
		if(resp.data[-1:] == "\n"):
			resp.data = resp.data[:-1]
		names = resp.data.split("\n")
		return(names)
	def putRemote(self,pathbits,data):
		val = self.__doReq("put",pathbits[0],"/".join(pathbits[1:]),data)
		return(val)

